<?php

namespace ly;

use Exception;

const UA_ANDROID = 'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Mobile Safari/537.36';
const UA_WEB     = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.31';
const UA_IPHONE  = 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1';
const UA_IE      = 'Mozilla/5.0 (Windows NT 10.0; Trident/7.0; rv:11.0) like Gecko';
const UA_MACOS   = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36';
const UA_WEIXIN  = 'Mozilla/5.0 (Linux; Android 11; Pixel 5; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/117.0.0.0 Mobile Safari/537.36 XWEB/1160083 MMWEBSDK/20231202 MMWEBID/6954 MicroMessenger/8.0.47.2560(0x28002F50) WeChat/arm64 Weixin NetType/WIFI Language/zh_CN ABI/arm64';
const UA_APPLET  = UA_WEIXIN . ' MiniProgramEnv/android';
const UA_SYMBIAN = 'Mozilla/5.0 (Symbian/3; Series60/5.2 NokiaN8-00/012.002; Profile/MIDP-2.1 Configuration/CLDC-1.1 ) AppleWebKit/533.4 (KHTML, like Gecko) NokiaBrowser/7.3.0 Mobile Safari/533.4 3gpp-gba';

/**
 * curl网络请求封装
 *
 * @author  涟漪 QQ：29397395
 * @version 2024.04.06
 * @package \ly\Curl
 */
class Curl {
	private $_cookie_jar;
	/**
	 * @var bool 请求状态标记
	 */
	private $_status = false;
	/**
	 * @var array 请求结果数据
	 */
	private $_info = [];
	/**
	 * @var string 通过设置limit(CURLOPT_WRITEFUNCTION)得到的数据
	 */
	private $_data = null;
	/**
	 * @var null|Callable 拦截器回调函数
	 */
	private $_callback = null;
	/**
	 * @var bool|string 对params额外要编码的单字节字符
	 */
	private $_en_char = true;
	/**
	 * @var null|string 响应数据编码类型
	 */
	private $_charset;
	/**
	 * @var string 请求Content-Type
	 */
	private $_content_type = 'application/x-www-form-urlencoded';
	/**
	 * @var string 请求方法
	 */
	private $_method = 'GET';
	/**
	 * @var int    错误代码，0：无错误
	 */
	private $_errno = 0;
	/**
	 * @var string 错误信息
	 */
	private $_error = '';

	/**
	 * @var array<string,string> 请求头
	 */
	public $mHeader = [];
	/**
	 * @var array<string,string> 请求cookie
	 */
	public $mCookie = [];

	/**
	 * @var string|string[]> url地址
	 */
	public $mUrl = null;

	/**
	 * @var null|string|array url参数
	 */
	public $mParams = null;

	/**
	 * @var null|string|array post数据
	 */
	public $mFields = null;

	/**
	 * @var array 默认CURL选项
	 */
	public $options = [
		CURLOPT_HEADER         => true,  // 输出响应头
		CURLINFO_HEADER_OUT    => true,  // 输出请求头
		CURLOPT_TIMEOUT        => 5,     // 超时(秒)
		CURLOPT_CONNECTTIMEOUT => 5,     // 连接超时
		CURLOPT_RETURNTRANSFER => true,  // 输出数据流
		CURLOPT_FOLLOWLOCATION => true,  // 自动跳转追踪
		CURLOPT_AUTOREFERER    => true,  // 自动设置来路信息
		CURLOPT_SSL_VERIFYPEER => false, // 认证证书检查
		CURLOPT_SSL_VERIFYHOST => false, // 检查SSL加密算法
		CURLOPT_NOSIGNAL       => false, // 忽略所有传递的信号
		CURLOPT_ENCODING       => 'gzip, deflate'
	];

	private function __construct () {
		$this->_cookie_jar = charset(__DIR__, "GBK") . DIRECTORY_SEPARATOR . 'cookie.txt';
		$this->ua(true);
	}

	/**
	 * 实例化
	 *
	 * @return $this
	 */
	public static function init () {
		return new self();
	}

	/**
	 * 设置 CURLOPT_ 选项
	 *
	 * @param  int|array  $key
	 * @param  mixed      $val
	 *
	 * @return $this
	 */
	public function opt ($key, $val = null) {
		if (is_array($key)) {
			foreach ($key as $k => $v) $this->opt($k, $v);

			return $this;
		}
		if (!is_int($key)) return $this;
		if ($val === null) return $this->del($key);
		if (@$this->options[$key] !== $val) $this->reset()->options[$key] = $val;

		return $this;
	}

	/**
	 * 删除 CURLOPT_ 选项
	 *
	 * @param  int  ...$keys
	 *
	 * @return $this
	 */
	public function del (...$keys) {
		foreach ($keys as $key)
			if (array_key_exists($key, $this->options)) {
				unset($this->options[$key]);
				$this->reset();
			}

		return $this;
	}

	/**
	 * Cookie自动化，默认关闭
	 *
	 * @param  bool  $bool
	 *
	 * @return $this
	 */
	public function autoCookie ($bool = false) {
		$val = $bool ? $this->_cookie_jar : null;

		return $this->opt([CURLOPT_COOKIEFILE => $val, CURLOPT_COOKIEJAR => $val]);
	}

	/**
	 * 设置超时时间
	 *
	 * @param  int  $timeout
	 * @param  int  $connect_timeout
	 *
	 * @return $this
	 */
	public function timeout ($timeout = 5, $connect_timeout = 5) {
		return $this->opt([CURLOPT_TIMEOUT => $timeout, CURLOPT_CONNECTTIMEOUT => $connect_timeout]);
	}

	/**
	 * 自动重定向，默认开启
	 *
	 * @param  bool|int  $redirect
	 *
	 * @return $this
	 */
	public function location ($redirect = true) {
		return $this->redirect($redirect);
	}

	/**
	 * 自动重定向，默认开启
	 *
	 * @param  bool|int  $redirect
	 *
	 * @return $this
	 */
	public function redirect ($redirect = true) {
		if (is_int($redirect) && $redirect >= 0) $this->opt(CURLOPT_MAXREDIRS, $redirect);

		return $this->opt(CURLOPT_FOLLOWLOCATION, boolval($redirect));
	}

	/**
	 * 自定义User-Agent
	 *
	 * @param  bool|string  $agent
	 *
	 * @return $this
	 */
	public function ua ($agent = true) {
		return $this->agent($agent);
	}

	/**
	 * 自定义User-Agent
	 *
	 * @param  null|bool|string  $agent
	 *
	 * @return $this
	 */
	public function agent ($agent = true) {
		if (strval($agent) === '') return $this->del(CURLOPT_USERAGENT);

		if ($agent === true || strcasecmp($agent, 'auto') === 0)
			return $this->opt(CURLOPT_USERAGENT, @$_SERVER['HTTP_USER_AGENT']);

		$map = [
			'/^(m|wap|h5|android|mobile)$/i'         => UA_ANDROID,
			'/^(pc|web|win|computer|chrome|edge)$/i' => UA_WEB,
			'/^(iphone|ios)$/i'                      => UA_IPHONE,
			'/^(ie)$/i'                              => UA_IE,
			'/^(mac(os)?)$/i'                        => UA_MACOS,
			'/^(wx|weixin)$/i'                       => UA_WEIXIN,
			'/^(applet)$/i'                          => UA_APPLET,
			'/^(symbian)$/i'                         => UA_SYMBIAN
		];
		foreach ($map as $pattern => $ua) if (preg_match($pattern, $agent)) $agent = $ua;

		return $this->opt(CURLOPT_USERAGENT, $agent);
	}

	/**
	 * 自定义来路(Referer)
	 *
	 * @param  string  $ref
	 *
	 * @return $this
	 */
	public function referer ($ref = true) {
		return $this->ref($ref);
	}

	/**
	 * 自定义来路(Referer)
	 *
	 * @param  bool|string  $ref
	 *
	 * @return $this
	 */
	public function ref ($ref = true) {
		if (is_bool($ref)) return $this->opt(CURLOPT_AUTOREFERER, $ref);

		return $this->opt(CURLOPT_REFERER, $ref);
	}

	/**
	 * 请求鉴权（Authorization: Bearer ***）
	 *
	 * @param  string  $auth
	 * @param  string  $type
	 *
	 * @return $this
	 */
	public function auth ($auth, $type = 'Basic') {
		return $this->addHeader('Authorization', $type === null || $type === '' ? $auth : $type . ' ' . $auth);
	}

	/**
	 * 设置 CURLOPT_ENCODING
	 *
	 * @param  string  $encoding
	 *
	 * @return $this
	 */
	public function encoding ($encoding = 'gzip, deflate') {
		return $this->opt(CURLOPT_ENCODING, $encoding);
	}

	/**
	 * 设置请求头（重置）
	 *
	 * @param  string|string[]|array<string,mixed>  $key
	 * @param  string                               $value
	 *
	 * @return $this
	 */
	public function header ($key, $value = null) {
		return $this->reset('header')->addHeader($key, $value);
	}

	/**
	 * 追加请求头（不重置）
	 *
	 * @param  string|string[]|array<string,mixed>  $key
	 * @param  string                               $value
	 *
	 * @return $this
	 */
	public function addHeader ($key, $value = null) {
		if (is_array($key)) {
			$is_associative = array_is_associative($key);
			foreach ($key as $k => $v) $is_associative ? $this->addHeader($k, $v) : $this->addHeader($v);
		} else if (is_string($key) && $key !== '') {
			if ($value === null) {
				$key = self::parse_header($key, true);
				foreach ($key as $k => $v) $this->addHeader($k, $v);
			} else {
				$key = self::ucwords($key);
				if ($key === 'User-Agent') $this->ua($value);
				else if ($key === 'Referer') $this->ref($value);
				else if ($key === 'Cookie') $this->addCookie($value);
				else if ($key === 'Accept-Encoding') $this->encoding($value);
				else $this->reset()->mHeader[$key] = $value;
			}
		}

		return $this;
	}

	/**
	 * 移除请求头
	 *
	 * @param  string  ...$keys
	 *
	 * @return $this
	 */
	public function removeHeader (...$keys) {
		foreach ($keys as $key) {
			$key = self::ucwords($key);
			if ($key === 'User-Agent') $this->ua(null);
			else if ($key === 'Referer') $this->ref(null);
			else if ($key === 'Cookie') $this->mCookie = [];
			else if ($key === 'Accept-Encoding') $this->encoding(null);
			else unset($this->mHeader[$key]);
		}

		return $this;
	}

	/**
	 * 设置cookie（重置）
	 *
	 * @param  string|string[]|array<string,mixed>  $name
	 * @param  string                               $value
	 *
	 * @return $this
	 */
	public function cookie ($name, $value = null) {
		return $this->reset('cookie')->addCookie($name, $value);
	}

	/**
	 * 追加cookie（不重置）
	 *
	 * @param  string|string[]|array<string,mixed>  $name
	 * @param  string                               $value
	 *
	 * @return $this
	 */
	public function addCookie ($name, $value = null) {
		if (!is_array($name) && !is_string($name)) return $this;

		if (is_array($name)) {
			$is_associative = array_is_associative($name);
			foreach ($name as $k => $v) $is_associative ? $this->addCookie($k, $v) : $this->addCookie($v);
		} else if ($value === null) {
			$name = self::parse_str($name, ';');
			foreach ($name as $k => $v) $this->addCookie($k, $v);
		} else if ($name !== '' && @$this->mCookie[$name] !== $value) {
			$this->reset()->mCookie[$name] = rawurldecode($value);
		}

		return $this;
	}

	/**
	 * 请求头：X-Requested-With
	 *
	 * @param  string  $val
	 *
	 * @return $this
	 */
	public function ajax ($val = 'XMLHttpRequest') {
		return $this->xhr($val);
	}

	/**
	 * 请求头：X-Requested-With
	 *
	 * @param  string  $val
	 *
	 * @return $this
	 */
	public function xhr ($val = 'XMLHttpRequest') {
		return $this->addHeader('X-Requested-With', $val);
	}

	/**
	 * 响应数据编码
	 *
	 * @param  string  $charset
	 *
	 * @return $this
	 */
	public function charset ($charset = null) {
		$this->_charset = $charset;

		return $this;
	}

	/**
	 * 对params额外要编码的单字节字符
	 *
	 * @param  true|string  $en_char  true默认：+
	 *
	 * @return $this
	 */
	public function en_char ($en_char = true) {
		$this->_en_char = $en_char;

		return $this->reset();
	}

	/**
	 * 限制取响应数据大小
	 *
	 * @param  int|null  $size  大于0：限制，否则取消限制
	 *
	 * @return $this
	 */
	public function limit ($size = 1024 * 1024) {
		$size = intval($size);

		return $size > 0 ? $this->opt([
			CURLOPT_BUFFERSIZE       => 128,
			CURLOPT_NOPROGRESS       => false,
			CURLOPT_PROGRESSFUNCTION => function ($resource, $downloadSize, $downloadedBytes, $uploadSize, $uploadedBytes) use ($size) {
				return $downloadedBytes >= $size;
			},
			CURLOPT_WRITEFUNCTION    => function ($resource, $str) {
				$this->_data .= $str;

				return strlen($str);
			}
		]) : $this->del(CURLOPT_BUFFERSIZE, CURLOPT_NOPROGRESS, CURLOPT_PROGRESSFUNCTION, CURLOPT_WRITEFUNCTION);
	}

	/**
	 * 设置 请求方法
	 *
	 * @param  string  $method  GET、POST、HEAD、PUT等
	 *
	 * @return $this
	 */
	public function method ($method) {
		$method = strtoupper($method);
		if ($method === '') return $this;
		$this->_method = $method;

		return $this->opt(CURLOPT_CUSTOMREQUEST, $this->_method);
	}

	/**
	 * 获取请求方法
	 *
	 * @return string
	 */
	public function getMethod ($lower = false) {
		return $lower ? strtolower($this->_method) : $this->_method;
	}

	/**
	 * 设置 CURLOPT_NOBODY
	 *
	 * @param  bool  $nobody
	 *
	 * @return $this
	 */
	public function nobody ($nobody = true) {
		return $this->opt(CURLOPT_NOBODY, $nobody);
	}

	/**
	 * HEAD请求
	 *
	 * @param  string|array       $url     url地址
	 * @param  null|string|array  $params  查询参数
	 *
	 * @return $this
	 */
	public function head ($url, $params = null) {
		return $this->method('HEAD')->nobody()->raw($url, null, $params, $this->_content_type);
	}

	/**
	 * GET请求
	 *
	 * @param  string|array       $url     url地址
	 * @param  null|string|array  $params  查询参数
	 *
	 * @return $this
	 */
	public function get ($url, $params = null) {
		return $this->method('GET')->raw($url, null, $params, $this->_content_type);
	}

	/**
	 * PUT请求
	 *
	 * @param  string|array       $url     url地址
	 * @param  null|string|array  $fields  表单参数
	 * @param  null|string|array  $params  查询参数
	 *
	 * @return $this
	 */
	public function put ($url, $fields = null, $params = null) {
		return $this->method('PUT')->raw($url, $fields, $params, $this->_content_type);
	}

	/**
	 * POST请求
	 *
	 * @param  string|array       $url     url地址
	 * @param  null|string|array  $fields  表单参数
	 * @param  null|string|array  $params  查询参数
	 *
	 * @return $this
	 */
	public function post ($url, $fields = null, $params = null) {
		return $this->method('POST')->raw($url, $fields, $params, $this->_content_type);
	}

	/**
	 * JSON请求
	 *
	 * @param  string|array       $url     url地址
	 * @param  null|string|array  $fields  表单参数
	 * @param  null|string|array  $params  查询参数
	 *
	 * @return $this
	 */
	public function json ($url, $fields = null, $params = null) {
		return $this->method('POST')->raw($url, $fields, $params, 'json');
	}

	/**
	 * 上传文件请求
	 *
	 * @param  string|array       $url     url地址
	 * @param  array              $fields  表单参数
	 * @param  null|string|array  $params  查询参数
	 *
	 * @return $this
	 */
	public function upload ($url, $fields = [], $params = null) {
		return $this->method('POST')->raw($url, $fields, $params, 'form-data');
	}

	/**
	 * @param  string|array       $url     url地址
	 * @param  null|string|array  $fields  表单参数
	 * @param  null|string|array  $params  查询参数
	 * @param  null|string        $type    请求头Content-Type，常用的可简写/后面
	 *
	 * @return $this
	 */
	public function raw ($url, $fields = null, $params = null, $type = 'urlencoded') {
		if (!is_array($params)) $params = self::parse_str($params);
		if (!is_string($type)) $type = 'urlencoded';

		$this->mUrl    = is_array($url) ? $url : rtrim($url, '&');
		$this->mParams = $params;
		$this->mFields = $fields;

		return $this->contentType($type);
	}

	/**
	 * 拦截器
	 *
	 * @param  null|Callable  $callback  callable(\ly\Curl $curl, int $index, resource $ch)
	 *
	 * @return $this
	 */
	public function interceptor ($callback = null) {
		$this->_callback = $callback !== null && is_callable($callback) ? $callback : null;

		return $this->reset();
	}

	/**
	 * 取本地cookie_jar文件中的cookie
	 *
	 * @param  string  $url
	 *
	 * @return array|null
	 */
	public function getLocalCookie ($url) {
		$url = strval($url);
		if ($url === '') return null;

		$cookie = @file_get_contents($this->_cookie_jar);
		$arr    = parse_url($url);
		$host   = preg_replace('/.*?\.(?=.+?\..+$)/', '', $arr['host']);
		$p      = preg_match_all('/' . preg_quote($host, '/') . '(?:\s+([^\s\r\n]+))(?:\s+([^\s\r\n]+))(?:\s+([^\s\r\n]+))(?:\s+([^\s\r\n]+))(?:\s+([^\s\r\n]+))(?:\s+([^\s\r\n]+))(?:\r*\n|$)/',
			rawurldecode($cookie), $m);

		return $p > 0 ? array_combine($m[5], $m[6]) : null;
	}

	/**
	 * 取响应cookie
	 *
	 * @param  null|string  $name   字段：键
	 * @param  null|string  $value  字段：值
	 * @param  int|null     $index  null取全部，负数从末尾取
	 *
	 * @return array|string|null
	 */
	public function getCookie ($name = null, $value = 'value', $index = -1) {
		$cookie = $this->all('cookie');
		if ($cookie === null) return null;

		$name  = strval($name);
		$value = strval($value);
		if ($this->_info['redirect_count'] > 0) {
			if (!is_int($index)) {
				if ($value === '' && $name === '') return $cookie;

				if ($name !== '') $cookie = array_map(function ($items) use ($name) {
					if (empty($items)) return $items;

					return array_values(array_filter($items, function ($item) use ($name) {
						return $item['name'] === $name;
					}));
				}, $cookie);
				if ($value !== '') $cookie = array_map(function ($items) use ($name, $value) {
					if (!is_array($items)) return $items;
					$items = arr_column($items, $value, $name === '' ? 'name' : null, true);

					return !empty($items) ? count($items) === 1 ? current($items) : $items : null;
				}, $cookie);

				return $cookie;
			}

			if ($index < 0) $index = count($cookie) + $index;
			$cookie = @$cookie[$index];
		}

		if (($name === '' && $value === '') || $cookie === null) return $cookie;

		if ($name !== '') $cookie = array_values(array_filter($cookie, function ($item) use ($name) {
			return $item['name'] === $name;
		}));
		if ($value === '') return count($cookie) === 1 ? current($cookie) : $cookie;

		$cookie = arr_column($cookie, $value, $name === '' ? 'name' : null, true);
		if (empty($cookie)) return null;

		return $name !== '' && count($cookie) === 1 ? current($cookie) : $cookie;
	}

	/**
	 * 取响应头数据
	 *
	 * @param  int|null  $index  null取全部，负数从末尾取
	 *
	 * @return string|array
	 */
	public function getHeader ($index = -1) {
		$header = $this->all('header');
		if (!is_array($header) || $this->_info['redirect_count'] === 0 || !is_int($index)) return $header;

		if ($index < 0) $index = count($header) + $index;

		return @$header[$index];
	}

	/**
	 * 取响应头数据
	 *
	 * @param  null|string  $key
	 * @param  int|null     $index  null取全部，负数从末尾取
	 *
	 * @return array|string|null
	 */
	public function getHeaders ($key = null, $index = -1) {
		$headers = $this->all('headers');
		if (!is_array($headers)) return $headers;

		$key = self::ucwords($key);
		if ($this->_info['redirect_count'] > 0) {
			if (!is_int($index)) return $key === ''
				? $headers
				: array_map(function ($item) use ($key) {
					return @$item[$key];
				}, $headers);

			if ($index < 0) $index = count($headers) + $index;
			$headers = @$headers[$index];
		}

		return $key === '' ? $headers : @$headers[$key];
	}

	/**
	 * 取html源码 (HTML 实体转换为字符)
	 *
	 * @param  string  $charset  编码
	 *
	 * @return string
	 */
	public function html ($charset = null) {
		return @html_entity_decode($this->response($charset), ENT_HTML5);
	}

	/**
	 * 取响应数据json对象
	 *
	 * @param  bool  $associative
	 *
	 * @return false|mixed
	 */
	public function obj ($associative = false) {
		return is_json($this->response(), $associative);
	}

	/**
	 * 取响应数据SimpleXMLElement对象
	 *
	 * @param  null|bool  $associative
	 * @param  int        $options
	 *
	 * @return false|mixed|\SimpleXMLElement
	 */
	public function xml ($associative = null, $options = LIBXML_NOCDATA) {
		$res    = $this->response();
		$result = simplexml_load_string(trim($res), 'SimpleXMLElement', $options);
		if (is_bool($associative)) $result = is_json(json_encode($result), $associative);

		return $result;
	}

	/**
	 * 取响应数据并去掉html标签（strip_tags）
	 *
	 * @param  string  $charset  编码
	 *
	 * @return null|string
	 */
	public function text ($charset = null) {
		return @strip_tags($this->html($charset));
	}

	/**
	 * 获取响应数据
	 *
	 * @param  string  $charset  编码
	 *
	 * @return null|string
	 */
	public function content ($charset = null) {
		return $this->response($charset);
	}

	/**
	 * 获取响应数据
	 *
	 * @param  string  $charset  编码
	 *
	 * @return null|string
	 */
	public function response ($charset = null) {
		return $this->charset($charset === null ? $this->_charset : $charset)->exec();
	}

	/**
	 * 获取响应代码
	 *
	 * @return int
	 */
	public function code () {
		return $this->all('http_code');
	}

	/**
	 * 获取重定向url地址
	 *
	 * @return string
	 */
	public function redirect_url () {
		return $this->all('redirect_url');
	}

	/**
	 * 获取最后1个有效url地址
	 *
	 * @return string
	 */
	public function url () {
		return $this->all('url');
	}

	/**
	 * 设置请求Content-Type
	 *
	 * @param  null|string  $content_type  null:删除
	 *
	 * @return $this
	 */
	public function contentType ($content_type = null) {
		$types        = [
			'urlencoded' => 'application/x-www-form-urlencoded',
			'form-data'  => 'multipart/form-data',
			'json'       => 'application/json',
			'xml'        => 'application/xml',
			'javascript' => 'application/javascript',
			'plain'      => 'text/plain',
			'html'       => 'text/html'
		];
		$content_type = !is_string($content_type) || $content_type === '' ? null : strtolower($content_type);
		if (array_key_exists($content_type, $types)) $content_type = $types[$content_type];
		$this->_content_type = $content_type;

		return $this->addHeader('Content-Type', $content_type);
	}

	/**
	 * 取响应Content-Type
	 *
	 * @return string
	 */
	public function getContentType () {
		return $this->all('content_type');
	}

	/**
	 * 取响应Content-Length
	 *
	 * @param  int  $index  -1取最后一个
	 *
	 * @return int|string
	 */
	public function getContentLength ($index = -1) {
		$content_length = $this->getHeaders('Content-Length', $index);
		$length         = intval($content_length);
		if ($content_length !== null && ($length !== PHP_INT_MAX || strval($length) === $content_length))
			$content_length = $length;

		return $content_length;
	}

	/**
	 * 取错误代码，0：无错误
	 *
	 * @return int
	 */
	public function errno () {
		$this->exec();

		return $this->_errno;
	}

	/**
	 * 取错误信息
	 *
	 * @return string
	 */
	public function error () {
		$this->exec();

		return $this->_error;
	}

	/**
	 * @param  string  $key
	 *
	 * @return mixed
	 */
	public function all ($key = null) {
		return $this->exec($key);
	}

	/**
	 * 下载文件
	 *
	 * @param  string  $file  文件目录或完整路径
	 * @param  string  $name  文件名
	 *
	 * @return string|true
	 */
	public function down ($file = null, $name = null) {
		$file = strval($file);
		$name = strval($name);
		if ($file === '') $file = './' . pathinfo($this->options[CURLOPT_URL], PATHINFO_BASENAME);
		else if ($name !== '') $file = $file . DIRECTORY_SEPARATOR . $name;

		$data = $this->limit(null)->nobody(false)->redirect()->charset(null)->exec();
		try {
			$fo = fopen($file, 'a');
			fwrite($fo, $data);
			fclose($fo);

			return true;
		} catch (Exception $e) {
			return $e->getMessage();
		}
	}

	/**
	 * 重置请求标记
	 *
	 * @param  string  ...$keys  charset,params,fields,header,cookie
	 *
	 * @return $this
	 */
	public function reset (...$keys) {
		foreach ($keys as $key) {
			switch ($key) {
				case 'charset':
					$this->_charset = null;
					break;
				case 'params':
					$this->mParams = null;
					break;
				case 'fields':
					$this->mFields = null;
					break;
				case 'header':
					$this->mHeader = [];
					break;
				case 'cookie':
					$this->mCookie = [];
					break;
			}
		}
		$this->_info   = [];
		$this->_status = false;
		$this->_errno  = 0;
		$this->_error  = '';

		return $this;
	}

	/**
	 * 并发请求
	 *
	 * @param  null|callable  $callback  回调函数 callable(\ly\Curl $curl, int $index, resource $ch)
	 *
	 * @return $this[]
	 * @throws \Exception
	 */
	public function multi ($callback = null) {
		if (!is_array($this->mUrl)) throw new Exception("url地址类型错误，应为：string[]或array<int,array{url:string,params:null|string|array,fields:null|string|array,method:null|string}>");

		$curls   = [];
		$mParams = $this->mParams;
		$mFields = $this->mFields;
		$_method = $this->getMethod();
		$_type   = $this->_content_type;
		foreach ($this->mUrl as $i => $val) {
			$url = $params = $fields = $method = $type = null;
			if (is_array($val)) extract($val);
			else $url = $val;

			if (!is_string($url) || !filter_var(url_encode($url), FILTER_VALIDATE_URL))
				throw new Exception("url地址类型错误，错误索引：{$i}");

			if ($params === null) $params = $mParams;
			else if (is_array($params) && is_array($mParams)) $params = array_merge($mParams, $params);
			if ($fields === null) $fields = $mFields;
			else if (is_array($fields) && is_array($mFields)) $fields = array_merge($mFields, $fields);
			if ($type === null) $type = $_type;
			if ($method === null) $method = $_method;

			$curls[$i] = clone $this;
			$curls[$i]->method($method)->raw($url, $fields, $params, $type);
		}

		return self::multiple($callback, ...$curls);
	}

	/**
	 * 并发请求
	 *
	 * @param  null|callable  $callback  回调函数 callable(\ly\Curl $curl, int $index, resource $ch)
	 * @param  \ly\Curl       ...$curls
	 *
	 * @return $this[]
	 * @throws \Exception
	 */
	public static function multiple ($callback = null, ...$curls) {
		if (empty($curls)) throw new Exception("参数不能为空");
		$mh   = curl_multi_init();
		$conn = [];
		foreach ($curls as $i => $curl) {
			if (!$curl instanceof self) throw new Exception("参数类型错误，错误参数索引：{$i}");
			$conn[$i] = $curl->reset()->build_options($i);
			curl_multi_add_handle($mh, $conn[$i]);
		}

		$running = null;
		do {
			curl_multi_exec($mh, $running);
		} while ($running > 0);

		foreach ($conn as $i => $ch) {
			if (null === ($data = curl_multi_getcontent($ch)) && $curls[$i]->_data !== null) $data = $curls[$i]->_data;
			$curls[$i]->_info  = curl_getinfo($ch);
			$curls[$i]->_errno = curl_errno($ch);
			$curls[$i]->_error = curl_error($ch);
			$curls[$i]->handle_response($data, $curls[$i]->_info);
			if (is_callable($callback)) call_user_func($callback, $curls[$i], $i, $ch);
			curl_multi_remove_handle($mh, $ch);
			curl_close($ch);
		}

		curl_multi_close($mh);

		return $curls;
	}

	/**
	 * 执行请求并返回数据
	 *
	 * @param  string|null  $key
	 *
	 * @return null|mixed
	 */
	private function exec ($key = 'response') {
		if (!$this->_status) {
			$ch   = $this->reset()->build_options();
			$data = curl_exec($ch);
			if (is_bool($data) && $this->_data !== null) $data = $this->_data;
			$this->_info  = curl_getinfo($ch);
			$this->_errno = curl_errno($ch);
			$this->_error = curl_error($ch);
			curl_close($ch);
			$this->handle_response($data, $this->_info);
		}
		if (!is_string($key) || $key === '') return $this->_info;

		return $key === 'response' ? charset(@$this->_info[$key], $this->_charset) : @$this->_info[$key];
	}

	/**
	 * 构建参数
	 *
	 * @param  int  $index
	 *
	 * @return false|resource
	 */
	private function build_options ($index = -1) {
		$ch = curl_init();
		if (is_callable($this->_callback)) call_user_func($this->_callback, $this, $index, $ch);

		$url    = url_encode(rtrim($this->mUrl, '&'), $this->_en_char);
		$params = $this->mParams;
		if (is_array($params)) $params = arr2params($params, true);
		if ($params !== null && $params !== '') {
			$arr          = parse_url($url);
			$arr['query'] = @$arr['query'] . '&' . $params;
			$arr['query'] = trim($arr['query'], '&');
			$url          = build_url($arr, $this->_en_char);
		}

		$fields = $this->mFields;
		$type   = $this->_content_type;
		if ($fields !== null && is_string($type) && preg_match('#^([\w-]+)/([^;]+)#i', $type, $m)) {
			$is_array = is_array($fields) || $fields instanceof \stdClass;
			switch ($m[0]) {
				case 'application/x-www-form-urlencoded':
					if ($is_array) $fields = arr2params($fields, true);
					break;
				case 'multipart/form-data':
					if (!$is_array) $fields = self::parse_str($fields);
					break;
				case 'application/json':
					if ($is_array) $fields = arr2json($fields);
					break;
				case 'application/xml':
					if ($is_array) {
						$xml = new \SimpleXMLElement('<root/>');
						array_walk_recursive($fields, [$xml, 'addChild']);
						$fields = $xml->asXML();
					}
					break;
				case 'application/javascript':
				case 'text/plain':
				case 'text/html':
					break;
			}
		}

		$arr = [];
		foreach ($this->mHeader as $key => $val) $arr[] = $key . ': ' . $val;
		$this->opt([
			CURLOPT_URL        => $url,
			CURLOPT_POSTFIELDS => $fields,
			CURLOPT_HTTPHEADER => empty($arr) ? null : $arr,
			CURLOPT_COOKIE     => empty($this->mCookie) ? null : arr2params($this->mCookie, '\'"+/=;', '; ')
		]);

		curl_setopt_array($ch, $this->options);
		$this->_status = true;

		return $ch;
	}

	/**
	 * 处理响应结果
	 *
	 * @param  string|false|null  $data
	 * @param  array              $info
	 */
	private function handle_response ($data, &$info) {
		$header_size = $info['header_size'];
		if (is_string($data) && $header_size > 0) {
			$redirect_count  = $info['redirect_count'];
			$header          = trim(substr($data, 0, $header_size));
			$info['header']  = preg_split('/(\r?\n){2,}/', $header, $redirect_count + 1);
			$cookie          = [];
			$info['headers'] = array_map(function ($val) use (&$cookie) {
				$header   = self::parse_header($val);
				$cookie[] = self::parse_header_cookie(@$header['Set-Cookie']);

				return $header;
			}, $info['header']);
			$info['cookie']  = $cookie;
			if ($redirect_count === 0) {
				$info['header']  = current($info['header']);
				$info['headers'] = current($info['headers']);
				$info['cookie']  = current($info['cookie']);
			}
			$info['response'] = substr($data, $header_size);
		}
		$this->_data = null;
	}

	/**
	 * 解析header字符串
	 *
	 * @param  string  $str  header字符串
	 * @param  bool    $req  是否请求头，否者响应头
	 *
	 * @return array
	 */
	private static function parse_header ($str, $req = false) {
		$header  = [];
		$str     = strval($str);
		$pattern = sprintf('/(?<=^|\n)\s*(?:([\w-]+?)\s*:\s*)%s(.*?)\s*(?=\r?\n|$)/', $req ? '' : '?');
		if (preg_match_all($pattern, $str, $m) > 0)
			foreach ($m[1] as $i => $key) {
				$key   = self::ucwords($key);
				$value = @$m[2][$i];
				if ($req) $header[$key] = $value;
				else if ($i === 0 && $key === '') $header[0] = $value;
				else if (!array_key_exists($key, $header)) $header[$key] = $value;
				else if (is_array($header[$key])) $header[$key][] = $value;
				else $header[$key] = [$header[$key], $value];
			}

		return $header;
	}

	/**
	 * 解析Cookie字符串
	 *
	 * @param  string|array  $cookie
	 *
	 * @return array|null
	 */
	private static function parse_header_cookie ($cookie) {
		$parse_cookie = function ($str) {
			$p      = preg_match_all('/(?<=^|;)\s*([^=]+?)\s*(=\s*(.*?))?\s*(?=;|$)/', $str, $m);
			$i      = 0;
			$cookie = [];
			while ($i < $p) {
				$key = rawurldecode($m[1][$i]);
				$val = rawurldecode($m[3][$i]);
				if ($i === 0) {
					$cookie['name']  = $key;
					$cookie['value'] = $val;
				} else if (strcasecmp($key, 'Expires') === 0 || strcasecmp($key, 'Max-Age') === 0)
					$cookie[$key] = false === ($d = date_create($val, timezone_open("Asia/Shanghai")))
						? intval($val) : $d->getTimestamp();
				else if (strcasecmp($key, 'HttpOnly') === 0 || strcasecmp($key, 'Secure') === 0)
					$cookie[$key] = true;
				else $cookie[$key] = $val;
				$i++;
			}

			return $cookie;
		};

		if (is_string($cookie)) $cookie = [$cookie];
		else if (!is_array($cookie)) return null;

		$cookies = [];
		foreach ($cookie as $item) {
			$cookie = $parse_cookie($item);
			if (!empty($cookie)) $cookies[] = $cookie;
		}

		return $cookies;
	}

	public static function parse_str ($str, $separator = '&', $splice = '=') {
		$ret = [];
		$str = trim(strval($str), $separator . $splice);
		$arr = explode($separator, $str);
		foreach ($arr as $val) {
			$arr2 = explode($splice, trim($val), 2);
			if ($arr2 && $arr2[0] !== '')
				$ret[rawurldecode($arr2[0])] = isset($arr2[1]) ? rawurldecode($arr2[1]) : null;
		}

		return $ret;
	}

	/**
	 * 英文单词首字母大写
	 *
	 * @param  string  $str
	 * @param  string  $separators  分割字符
	 *
	 * @return string
	 */
	public static function ucwords ($str, $separators = " -\t\r\n\f\v") {
		return ucwords(strval($str), $separators);
	}
}